{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "view-in-github"
   },
   "source": [
    "<a href=\"https://colab.research.google.com/github/lollcat/fab-torch/blob/dev-loll/experiments/gmm/fab_gmm.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "o7gv_8Zk18XQ",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "<a href=\"https://colab.research.google.com/github/lollcat/fab-torch/blob/master/experiments/gmm/fab_gmm.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "pIuF7gAmLbpI",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Setup Repo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "U21CxXjHRcsF",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "!git clone https://github.com/lollcat/fab-torch.git"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "tmsNqL0lRwGa",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import os\n",
    "os.listdir()\n",
    "os.chdir(\"fab-torch\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "d7eC5CsoRs_d",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "!pip install --upgrade ."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "1Ajs-kTgLeWU",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Let's go!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "TeFLDH5mLhv9",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Lzkmrn81LajP",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import normflows as nf\n",
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "import numpy as np\n",
    "\n",
    "from fab import FABModel, HamiltonianMonteCarlo, Metropolis\n",
    "from fab.utils.logging import ListLogger\n",
    "from fab.utils.plotting import plot_history, plot_contours, plot_marginal_pair\n",
    "from fab.target_distributions.gmm import GMM\n",
    "from fab.utils.prioritised_replay_buffer import PrioritisedReplayBuffer\n",
    "from fab import Trainer, PrioritisedBufferTrainer\n",
    "from fab.utils.plotting import plot_contours, plot_marginal_pair\n",
    "\n",
    "\n",
    "from experiments.make_flow import make_wrapped_normflow_realnvp"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "OHtMPbFlMKvd",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Setup Target distribution"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "kkr2CqqDMRBn",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "dim = 2\n",
    "n_mixes = 40\n",
    "loc_scaling = 40.0  # scale of the problem (changes how far apart the modes of each Guassian component will be)\n",
    "log_var_scaling = 1.0 # variance of each Gaussian\n",
    "seed = 0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "qM9PUDE3MMoA",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "torch.manual_seed(0)  # seed of 0 for GMM problem\n",
    "target = GMM(dim=dim, n_mixes=n_mixes,\n",
    "              loc_scaling=loc_scaling, log_var_scaling=log_var_scaling,\n",
    "              use_gpu=True, true_expectation_estimation_n_samples=int(1e5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "oxMmszREPEY1",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# plot target\n",
    "target.to(\"cpu\")\n",
    "fig, ax = plt.subplots()\n",
    "plotting_bounds = (-loc_scaling * 1.4, loc_scaling * 1.4)\n",
    "plot_contours(target.log_prob, bounds=plotting_bounds, n_contour_levels=80, ax=ax, grid_width_n_points=200)\n",
    "target.to(\"cuda\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "bJmoBOJ8REZO",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Create FAB model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "xp58k3FMQ3Qf",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# hyper-parameters\n",
    "\n",
    "# Flow\n",
    "n_flow_layers = 15\n",
    "layer_nodes_per_dim = 40\n",
    "lr = 1e-4\n",
    "max_gradient_norm = 100.0\n",
    "batch_size = 128\n",
    "n_iterations = 4000\n",
    "n_eval = 10\n",
    "eval_batch_size = batch_size * 10\n",
    "n_plots = 10 # number of plots shows throughout tranining\n",
    "use_64_bit = True\n",
    "alpha = 2.0\n",
    "\n",
    "# AIS\n",
    "# By default we use a simple metropolis mcmc transition with a fixed step size.\n",
    "# Can switch this to 'hmc' to improve training efficiency. \n",
    "transition_operator_type = \"metropolis\" \n",
    "n_intermediate_distributions = 1\n",
    "metropolis_step_size = 5.0\n",
    "\n",
    "# buffer config\n",
    "n_batches_buffer_sampling = 4\n",
    "maximum_buffer_length = batch_size * n_batches_buffer_sampling * 100\n",
    "min_buffer_length = batch_size * n_batches_buffer_sampling * 10\n",
    "\n",
    "# target p^\\alpha q^{a-\\alpha} as target for AIS. \n",
    "min_is_target = True\n",
    "p_target = not min_is_target # Whether to use p as the target. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "3P5c29Rayd2B",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "if use_64_bit:\n",
    "    torch.set_default_dtype(torch.float64)\n",
    "    target = target.double()\n",
    "    print(f\"running with 64 bit\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "MRJx0FhTRKIF",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Setup flow"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "ptJrkMn5Qz2F",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "flow = make_wrapped_normflow_realnvp(dim, n_flow_layers=n_flow_layers, \n",
    "                                 layer_nodes_per_dim=layer_nodes_per_dim,\n",
    "                                act_norm = False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "bNhmNi0zRMT2",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Setup Transition operator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "aAfUX8rgQ9XG",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "if transition_operator_type == \"hmc\":\n",
    "    # very lightweight HMC.\n",
    "    transition_operator = HamiltonianMonteCarlo(\n",
    "            n_ais_intermediate_distributions=n_intermediate_distributions,\n",
    "            dim=dim,\n",
    "            base_log_prob=flow.log_prob,\n",
    "            target_log_prob=target.log_prob,\n",
    "            alpha=alpha,\n",
    "            p_target=p_target,\n",
    "        n_outer=1,\n",
    "        epsilon=1.0, L=5)\n",
    "elif transition_operator_type == \"metropolis\":\n",
    "    transition_operator = Metropolis(            \n",
    "        n_ais_intermediate_distributions=n_intermediate_distributions,\n",
    "        dim=dim,\n",
    "        base_log_prob=flow.log_prob,\n",
    "        target_log_prob=target.log_prob,\n",
    "        p_target=p_target,\n",
    "        alpha=alpha,\n",
    "        n_updates=1,\n",
    "        adjust_step_size=False,\n",
    "        max_step_size=metropolis_step_size, # the same for all metropolis steps \n",
    "        min_step_size=metropolis_step_size,\n",
    "        eval_mode=False,\n",
    "                                  )\n",
    "else:\n",
    "    raise NotImplementedError"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "oIQthDLyLkus",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "### Setup FAB model with prioritised replay buffer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "wFrJvytJcAm2",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# use GPU if available\n",
    "if torch.cuda.is_available():\n",
    "    flow.cuda()\n",
    "    transition_operator.cuda()\n",
    "    target.to(\"cuda\")\n",
    "    print(f\"Running with GPU\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "SgXAZZpCSAiK",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "fab_model = FABModel(flow=flow,\n",
    "                     target_distribution=target,\n",
    "                     n_intermediate_distributions=n_intermediate_distributions,\n",
    "                     transition_operator=transition_operator,\n",
    "                     alpha=alpha)\n",
    "optimizer = torch.optim.Adam(flow.parameters(), lr=lr)\n",
    "logger = ListLogger(save=False) # save training history"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "5onsUCTJbE-l",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Setup buffer.\n",
    "def initial_sampler():\n",
    "  # fill replay buffer using initialised model and AIS.\n",
    "    point, log_w = fab_model.annealed_importance_sampler.sample_and_log_weights(\n",
    "            batch_size, logging=False)\n",
    "    return point.x, log_w, point.log_q\n",
    "buffer = PrioritisedReplayBuffer(dim=dim, max_length=maximum_buffer_length,\n",
    "                      min_sample_length=min_buffer_length,\n",
    "                      initial_sampler=initial_sampler)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "B2QJeaj5Ll4o",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def plot(fab_model, n_samples = 128):\n",
    "    fig, axs = plt.subplots(1, 3, figsize=(12, 4))\n",
    "    target.to(\"cpu\")\n",
    "    plot_contours(target.log_prob, bounds=plotting_bounds, ax=axs[0], n_contour_levels=50, grid_width_n_points=200)\n",
    "    plot_contours(target.log_prob, bounds=plotting_bounds, ax=axs[1], n_contour_levels=50, grid_width_n_points=200)\n",
    "    plot_contours(target.log_prob, bounds=plotting_bounds, ax=axs[2], n_contour_levels=50, grid_width_n_points=200)\n",
    "    target.to(\"cuda\")\n",
    "\n",
    "    # plot flow samples\n",
    "    samples_flow = fab_model.flow.sample((n_samples,)).detach()\n",
    "    plot_marginal_pair(samples_flow, ax=axs[0], bounds=plotting_bounds)\n",
    "\n",
    "\n",
    "    # plot ais samples\n",
    "    samples_ais = fab_model.annealed_importance_sampler.sample_and_log_weights(n_samples,\n",
    "                                                                               logging=False)[0].x\n",
    "    plot_marginal_pair(samples_ais, ax=axs[1], bounds=plotting_bounds)\n",
    "    \n",
    "    # plot buffer samples\n",
    "    samples_buffer = buffer.sample(n_samples)[0].detach()\n",
    "    plot_marginal_pair(samples_buffer, ax=axs[2], bounds=plotting_bounds)\n",
    "    \n",
    "    axs[0].set_title(\"flow samples\")\n",
    "    axs[1].set_title(\"ais samples\")\n",
    "    axs[2].set_title(\"buffer samples\")\n",
    "    plt.show()\n",
    "    return [fig]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "ecjfGOS2bWEq",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "plot(fab_model) # Visualise model during initialisation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "zfjeD4Udb275",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Setup trainer.\n",
    "trainer = PrioritisedBufferTrainer(model=fab_model, optimizer=optimizer, \n",
    "              logger=logger, plot=plot,\n",
    "              buffer=buffer, \n",
    "              n_batches_buffer_sampling=n_batches_buffer_sampling,\n",
    "              max_gradient_norm=max_gradient_norm,\n",
    "              alpha=alpha,\n",
    "              w_adjust_max_clip=None)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "ailsWaOwdF5V",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "## Train model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "r3a8E010fJ_0",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "This problem is quite challenging for training, as the flow has a very poor initialisation, and therefore often places extremely small probability on samples in new modes.\n",
    "\n",
    "This causes some **numerical instability**: There are lots of NaN errors throughout training, due to (1) numerical sinstability in the flow itself causing the flow to generate NaN samples or samples with large values that have NaN log prob under the target, as well as (2) sometimes AIS finds regions in the target with negligible mass under the flow.  However, these numerical instabilities do not prevent training from suceeding. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "IZ8ohs7a1vAu",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Now run!\n",
    "trainer.run(n_iterations=n_iterations, batch_size=batch_size, n_plot=n_plots, \\\n",
    "            n_eval=n_eval, eval_batch_size=eval_batch_size, save=False)  # note that the progress bar during training prints ESS w.r.t p^2/q. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "EH1zl3bQ1vAu",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "In the below plot of samples from the flow vs the target contours, and with the test set log prob throughout training, we see that the flow covers the target distribution quite well. It may be trained further to obtain even better results. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "87HoslTI1vAy",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Test set probability using samples from the target distribution.\n",
    "eval_iters = np.linspace(0, n_iterations, n_eval)\n",
    "plt.plot(eval_iters, logger.history['flow_test_set_mean_log_prob_p_target'])\n",
    "plt.ylabel(\"mean test set log prob\")\n",
    "plt.xlabel(\"eval iteration\")\n",
    "plt.yscale(\"symlog\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "BUxcXo501vAz",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "fig, axs = plt.subplots(1, 1, figsize=(5, 5))\n",
    "target.to(\"cpu\")\n",
    "plot_contours(target.log_prob, bounds=plotting_bounds, ax=axs, n_contour_levels=50, grid_width_n_points=200)\n",
    "target.to(\"cuda\")\n",
    "\n",
    "n_samples = 1000\n",
    "samples_flow = fab_model.flow.sample((n_samples,)).detach()\n",
    "plot_marginal_pair(samples_flow, ax=axs, bounds=plotting_bounds)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Bbz9vZ4F1vAz",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Training a flow by reverse KL divergence minimisation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "a-N5dD_x1vAz",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "loss_type = \"flow_reverse_kl\" # can set to \"target_foward_kl\" for training by maximum likelihood of samples from the GMM target."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "-e7Gg6Oj1vAz",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Create flow using the same architecture.\n",
    "flow = make_wrapped_normflow_realnvp(dim, n_flow_layers=n_flow_layers, \n",
    "                                 layer_nodes_per_dim=layer_nodes_per_dim,\n",
    "                                act_norm = False)\n",
    "optimizer = torch.optim.Adam(flow.parameters(), lr=lr)\n",
    "logger = ListLogger(save=False) # save training history"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "ieE0Maj01vA0",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# use GPU if available\n",
    "if torch.cuda.is_available():\n",
    "  flow.cuda()\n",
    "  transition_operator.cuda()\n",
    "  target.to(\"cuda\")\n",
    "  print(f\"Running with GPU\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "BIj8V3WR1vA0",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "n_iterations = int(3*(n_iterations)) # Training the flow by KL minimisation is cheaper per iteration, so we run it for more iterations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "Cey61dub1vA0",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "reverse_kld_model = FABModel(flow=flow,\n",
    "                     target_distribution=target,\n",
    "                     n_intermediate_distributions=n_intermediate_distributions,\n",
    "                     transition_operator=transition_operator,\n",
    "                     loss_type=loss_type,\n",
    "                     alpha=1.0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "0VZrrkep1vA7",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "def plot_flow_reverse_kld(fab_model, n_samples = 300):\n",
    "    fig, axs = plt.subplots(1,1, figsize=(4, 4))\n",
    "    target.to(\"cpu\")\n",
    "    plot_contours(target.log_prob, bounds=plotting_bounds, ax=axs, n_contour_levels=50, grid_width_n_points=200)\n",
    "    target.to(\"cuda\")\n",
    "\n",
    "    # plot flow samples\n",
    "    samples_flow = fab_model.flow.sample((n_samples,))\n",
    "    plot_marginal_pair(samples_flow, ax=axs, bounds=plotting_bounds)\n",
    "    \n",
    "    axs.set_title(\"flow samples\")\n",
    "    plt.show()\n",
    "    return [fig]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "MelWKsN_1vA8",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "trainer = Trainer(model=reverse_kld_model, optimizer=optimizer, logger=logger, plot=plot_flow_reverse_kld, max_gradient_norm=max_gradient_norm)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "T8SaB2BL1vA8",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# Now run!\n",
    "trainer.run(n_iterations=n_iterations, batch_size=batch_size, n_plot=n_plots, \\\n",
    "            n_eval=n_eval, eval_batch_size=eval_batch_size, save=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "GAUDI5lNweY_",
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "We evaluate the flow on samples from the target distribution, we see that because the flow trained by kl divergence minimisation is missing modes, the flow places NaN log prob on samples from the target. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "kTDrcaa5wK9g",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "logger.history[\"flow_test_set_mean_log_prob\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "cyLoMV8VvOSE",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "eval_iters = np.linspace(0, n_iterations, n_eval)\n",
    "plt.plot(eval_iters, logger.history[\"flow_test_set_mean_log_prob\"])\n",
    "plt.ylabel(\"mean test set log prob\")\n",
    "plt.xlabel(\"eval iteration\")\n",
    "plt.yscale(\"symlog\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "8Y_h1lGvvVMD",
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "include_colab_link": true,
   "provenance": []
  },
  "gpuClass": "standard",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
